Em várias linguagens existe um operador chamado switch, que funciona como uma sucessiva aplicação de if e else. Em Scala, o operador que faz isso é chamado de match. O exemplo abaixo demonstra um simples funcionamento do match, onde reescrevemos o encadeamento de estruturas de controle utilizando pattern matching:
In [ ]:
val x = 5
if(x==5) println("x = 5") //caso x seja 5
else if(x==10) println("x = 10") //caso x seja 10
else println("x não é 5 nem 10") //caso x não seja nem 5 nem 10
x match {
case 5 => println("x = 5") //caso x seja 5
case 10 => println("x = 10") //caso x seja 10
case _ => println("x não é 5 nem 10") //caso x não seja nem 5 nem 10
}
Diferente de outras linguagens, o pattern matching aceita não só comparação com números inteiros, mas sim com qualquer valor:
In [ ]:
val s: Any = "olá"
s match {
case "olá" => println("olá! :D")
case "oi" => println("oi! :)")
case i:Int => println("é o que mah?")
}
In [ ]:
abstract class Valor()
case class UmValor(a: Int) extends Valor //exemplo de uma classe com um valor
case class DoisValores(a: Int, b: String) extends Valor //exemplo de uma classe com dois valores
case class TresValores(a: Int, b: Int, c: Int) extends Valor //exemplo de uma classe com três valores
val m: Valor = new DoisValores(5,"abacaxi")
m match {
case UmValor(x) => print(s"Apenas um valor: $x")
case DoisValores(x,y) => print(s"Dois valores: $x, $y")
case TresValores(x,y,z) => print(s"Três valores: $x, $y, $z")
}
In [ ]:
//uma simples função que soma 2 inteiros
def somar(a: Int, b: Int): Int = a + b
//armazenando a função como variável
val x: (Int,Int) => Int = somar
No exemplo de código acima, podemos notar que x possui um tipo bem diferente do que vimos até agora. Essa é a notação de um tipo função em Scala. Inicialmente, temos os tipos dos parâmetros que a função vai receber e, por fim, o tipo que será retornado. No exemplo acima, x é uma função que recebe 2 inteiros e retorna outro inteiro, que é exatamente o que a função somar faz. Agora, podemos tratar x como uma função e realizar chamadas:
In [ ]:
println(x(1,2))
println(x(3,5))
Scala permite, literalmente, a definição de tipos. No exemplo acima, vimos que o valor x é uma função que recebe dois inteiros e retorna outro. A função somar segue essa mesma assinatura, assim como poderia seguir a função subtrair ou multiplicar. Para fins de legibilidade, podemos definir um tipo OperacaoBinaria que representa essa assinatura de função!
In [ ]:
type OperacaoBinaria = (Int, Int) => Int
def somar(a: Int, b: Int): Int = a + b
val x: OperacaoBinaria = somar
println(x(1,2))
println(x(3,5))
Até o momento nós apenas atribuímos funções pré-definidas a variáveis, ou seja, funções que passaram por um processo de declaração, onde receberam um identificador. O paradigma funcional permite a criação de funções sem declaração de identificador. Essas funções são chamadas de Funções Anônimas.
In [ ]:
def somar(a: Int, b: Int): Int = a + b
//x recebe uma função previamente declarada, a qual soma 2 inteiros
val x: (Int,Int) => Int = somar
//y recebe uma função sem antes ser declarada, a qual soma 2 inteiros
val y: (Int, Int) => Int = (a,b) => a + b
println(x(1,2))
println(y(1,2))
Para definirmos uma função anônima basta utilizar uma sintaxe similar a definição do tipo função: primeiro informa os parâmetros da função e, em seguida, o corpo do código.
OBS: É sempre necessário fazer a correspondência dos tipos, seja na hora de tipar a variável que receberá a função ou na própria função:
Uma aplicação comum de funções anônimas é quando precisamos mandar uma função para ser executada dentro de outra. Veremos isso mais a frente.
Nessa parte da aula, apresentaremos algumas das estruturas de dados de Scala. Trabalharemos com:
OBS:
In [ ]:
val x: (Int,Int) = (1,2)
val y: (Int,String,Int) = (10,"abc",23)
val z: (Int,String,Double,Int,Char) = (10,"abc",15.3,2,'o')
Para acessar os elementos de uma tupla, basta utilizar a notação ._n, onde n é a posição do elemento da tupla (começando de 1)
In [ ]:
println(x._1)
println(x._2)
Tuplas também permitem atribuição direta de cada elemento a uma variável:
In [ ]:
val (a,b,c) = y
println(s"a: $a, b: $b, c: $c")
Iteráveis são estruturas de dados que podem possuir vários elementos de um mesmo tipo (diferente da tupla, onde precisamos dizer o tipo de cada elemento). Essas estruturas possuem um conjunto de métodos de Alta Ordem (métodos que recebem outras funções que serão executadas em seu interior) para manipular seus elementos. Os tipos mais comuns de estruturas Iteráveis são Listas e Mapas.
Existem 2 tipos de Sequências em Scala: indexadas (Vector, Range, String,...) e lineares (List, Queue, Stream, Stack). Sequências indexadas são sequencias cujo índice do elemento (posição) está armazenado em uma estrutura ordenada (como uma árvore B, por exemplo), permitindo rápido acesso ao elemento em uma determinada posição. Sequências lineares são sequências onde cada elemento possui apenas seu próprio valor e uma referência a um próximo elemento.
A implementação de Lista em Scala trabalha com a estrutura cabeça e cauda: um elemento (cabeça) e uma referência ao restante da lista (cauda).
In [ ]:
val l = List[Int](1,2,3)
println(l.head)
println(l.tail)
Como a lista é uma sequência linear, não é comum que ela seja utilizada em cenários onde deseja-se acessar um elemento em uma determinada posição, pois, em sequências lineares, isso implica em percorrer todos os elementos até encontrar a posição desejada.
Listas em Scala possuem alguns operadores definidos para manipulá-las:
In [ ]:
val x = List[Int](1,2,3)
println(10 :: x) //adiciona ao início da lista
println(x :+ 10) //adiciona ao fim da lista
println(x ++ List[Int](4,5)) //adiciona a segunda lista ao final da primeira
println(x ::: List[Int](4,5)) //adiciona a segunda lista ao final da primeira
Existem alguns métodos comuns a todas as coleções em Scala, a fim de auxiliar na obtenção de informações:
In [ ]:
val x = List(1,2,3,2,4)
println(x.isEmpty) //está vazia
println(x.nonEmpty) //não está vazia
println(x.length) //tamanho
println(x.contains(1)) //pertinência
println(x.sum) //soma dos elementos
println(x.product) //produto dos elementos
println(x.distinct) //elementos distintos
println(x mkString(",")) //gera uma string utilizando um separador
println(x mkString("(",",",")")) //gera uma string utilizando um separador, uma string para iniciar e uma para finalizar
As coleções iteráveis em Scala também possuem vários métodos de Alta Ordem em comum. Mostraremos alguns desses métodos e como eles funcionam com as listas. O mesmo vale para outras coleções (com algumas modificações no mapa). Para os exemplos a seguir, utilizaremos a seguinte lista:
In [ ]:
val x = List[Int](1,2,3,4,5,6,7,8,9)
In [ ]:
x foreach (e => println(e))
OBS: em funções simples como a do exemplo acima, podemos utilizar uma notação simplificada de função anônima, onde a variável é substituída por wildcard( _ ):
In [ ]:
x foreach (println(_))
OBS2: Como a função println já é uma função que recebe apenas um parâmetro, podemos mandá-la diretamente como argumento:
In [ ]:
x foreach (println)
In [ ]:
x.filter(_%2 == 0) //apenas números pares
In [ ]:
x map (_*2) //lista com o dobro dos elementos
In [ ]:
List[Int](1,2,3) flatMap (0 to _) //para cada elemento e, retorna a sequência de 0 à e
In [ ]:
val y = x zip List[Int](9,8,7,6,5,4,3,2,1)
print(y)
In [ ]:
x zipWithIndex
In [ ]:
for((a,b) <- y) println(s"($a,$b)")
In [ ]:
x groupBy (_%2)
In [ ]:
val a = Map("a" -> 1, "b" -> 2, "c" -> 3)
val b = List(("d",4),("e",5),("f",6)).toMap
val c = List("g"->7,"h"->8,"i"->9).toMap
Mapas também possuem alguns métodos e operadores já definidos:
In [ ]:
println(a("b")) //obtenção de valor
println(a.getOrElse("j",-1)) //obtenção de valor com valor padrão (caso a chave não exista no mapa)
println(a + ("j" -> 10)) //adição de chave e valor
println(a + ("a" -> 5)) //caso a chave exista, será feita uma sobrescrita no valor
println(a - "a") //remoção de chave (e valor, por consequência)
println(a.keys) //chaves da coleção
println(a.values) //valores da coleção
Diferente das lista, as iterações feitas sobre mapas são aplicadas sobre o par (chave, valor):
In [ ]:
for((c,v) <- a) print(s"$c -> $v\t")
println()
println(a map (x => x._1 -> 2*x._2) mkString "\t")
println(a mkString "\t")
In [ ]:
type Set = Int => Boolean
object Operador{
def conjuntoUnitario(n: Int): Set =
x => x == n
def uniao(s: Set, t: Set): Set =
x => s(x) || t(x)
def interseccao(s: Set, t: Set): Set =
x => s(x) && t(x)
def diferenca(s: Set, t: Set): Set =
x => s(x) && !t(x)
def complemento(s: Set): Set =
x => !s(x)
}
val s = Operador.conjuntoUnitario(2)
val t = Operador.conjuntoUnitario(1)
//a função require precisa receber o valor true. Caso contrário, ela dispara uma exceção.
require(!s(1))
require(s(2))
require(!t(2))
require(t(1))
val u = Operador.uniao(s,t)
require(u(1))
require(u(2))
require(!u(3))
val i = Operador.interseccao(u,t)
require(i(1))
require(!i(2))
require(!i(3))
val d = Operador.diferenca(u,t)
require(d(2))
require(!d(1))
require(!d(3))
val c = Operador.complemento(u)
require(!c(1))
require(!c(2))
require(c(3))
require(c(4))
println("Parabéns! Sua implementação está correta!")
In [ ]:
//importando o objeto Source, que auxilia na manipulação de arquivos
import scala.io.Source
case class Amostra(sl: Double, sw: Double, pl: Double, pw: Double, c: String)
//lendo as linhas do arquivo
//NOTA: por padrão, o método Souce.fromFile retorna um Iterável que NÂO está totalmente em memória
//NOTA 2: é dado um drop(1) nas linhas para descartar a primeira linha, que é o cabeçalho do dataset
val lines = Source.fromFile("../01-python-jupyter-notebook/iris-dataset.txt").getLines.drop(1)
//utilize o método split(c) para dividir uma String, separando-as pelo caractere c
val amostras = lines
//separando os valores por tabulação
.map(_.split("\t"))
//filtrando apenas as linhas com 5 valores
.filter(_.length == 5)
//transformando as linhas em Amosrtas
.map(l => Amostra(l.head.toDouble, l(1).toDouble, l(2).toDouble, l(3).toDouble, l(4)))
.toList
//agrupando as amostras pela classe
val amostrasAgrupadas = amostras
.groupBy(_.c)
//definindo função que calcula a média de uma sequência de Doubles
def mean(values: Seq[Double]): Double = values.sum / values.length
//para cada classe e suas amostras, calcular a média de cada um dos 4 atributos
for((c,amostras) <- amostrasAgrupadas){
println(s"Analisando classe $c...")
val slMedia = mean(amostras.map(_.sl))
println(s"Media do comprimento da sépala: $slMedia")
val swMedia = mean(amostras.map(_.sw))
println(s"Media da largura da sépala: $swMedia")
val plMedia = mean(amostras.map(_.pl))
println(s"Media do comprimento da pétala: $plMedia")
val pwMedia = mean(amostras.map(_.pw))
println(s"Media da largura da pétala: $pwMedia")
println("-------------------")
}